说明
分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于 Redis 的分布式锁;3. 基于 ZooKeeper 的分布式锁。本文介绍基于 Redis 实现分布式锁。
关于实现分布式锁的三种方式,可以参考之前的博文: 分布式锁简单入门以及三种实现方式介绍
本文中的分布式锁通过注解的方式实现,可以自定义重试次数,锁超时时间等。
可靠性
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的 Redis 节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
并发问题
在没有使用分布式锁之前,如果有两个线程并发操作同一条数据,可能会出现并发问题(脏读、不可重复读、幻读)。
举个例子,下面是一段将数据的值 +1 的代码:
1 | public void addNum() { |
有两个线程同时访问:
1 | 15:56:06,241 INFO [stdout] (default task-39) start:0 |
可以看到 task-39 和 task-40 两个线程读取到的值均是 0,执行两次后,值为 1 ,并不是想要的 2。
具体执行的情况如下:
| task-39 | task-40 |
|---|---|
| 读取 num,num = 0 | |
| 读取 num,num = 0 | |
| num = num + 1 ,num 值变为 1 | |
| num = num + 1 ,num 值变为 1 | |
| 将 1 存入库中 | |
| 将 1 存入库中 |
如果是单机部署,那么可以用多线程的 18 般武艺来解决并发问题,比如加锁等,改动如下:
1 | public synchronized void addNum() { |
加了一个关键字 synchronized 就能解决单机下的并发问题,结果如下:
1 | 16:09:49,539 INFO [stdout] (default task-46) start:0 |
| task-46 | task-47 |
|---|---|
| 读取 num,num = 0 | |
| num = num + 1 ,num 值变为 1 | |
| 将 1 存入库中 | |
| 读取 num,num = 1 | |
| num = num + 1 ,num 值变为 2 | |
| 将 2 存入库中 |
如果集群部署的话,这种方式就无法解决了(每台机器的 JVM 无法共享,无法加锁),只能使用分布式锁。
分布式锁的实现
pom.xml:
1 | <dependency> |
首先定义一个接口,提供加锁、解锁两个方法。
1 | public interface IDistributedLock { |
定义一个抽象类,实现该接口:
1 | public abstract class AbstractDistributedLockImpl implements IDistributedLock { |
具体实现:
1 |
|
定义一个注解类:
1 | ({ElementType.METHOD}) |
定义切面:
1 |
|
配置文件:
application.properties:
1 | redis.pool.min-idle=0 |
applicationContext.xml:
1 | <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> |
注意下,最后一行别忘了加上,不然 AOP 不会起作用。如果报错,需要加上 xml 的命名空间,可以自行百度。
分布式锁的使用
修改之前的代码:
1 | (name = "#key") |
只需要加一个注解就行,可以用固定的 key 值,也可以用业务 ID 来作为锁的 key。这边用了业务 ID 来当 key,注解中也能自定义重试次数、超时时间等。
运行结果:
1 | 16:31:11,093 INFO [stdout] (default task-60) get lock success : key1_'default' |
可以看到 task-60 一开始就获取到了锁,而 task-61 一开始获取锁失败,进行了重试,直到 task-60 运行完释放锁后,task-61 才拿到锁,继续执行代码。
总结
利用 redis 的 SETNX 命令,可以实现分布式锁,并且具有超时自动解锁的功能,防止死锁。
利用 spring 的注解及 AOP 的特性,可以很方便地使用分布式锁。
使用 redis 来实现分布式锁,比其他两种方式(数据库乐观锁、zookeeper)更为简单。